Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00%
1 / 1
100.00%
4 / 4
CRAP
100.00%
171 / 171
PHP
100.00%
1 / 1
100.00%
4 / 4
37
100.00%
171 / 171
 __construct($cacheDir = null)
100.00%
1 / 1
3
100.00%
12 / 12
 getRenderer(Stylesheet $stylesheet)
100.00%
1 / 1
5
100.00%
21 / 21
 generate($xsl)
100.00%
1 / 1
21
100.00%
120 / 120
 fixEmptyElements(DOMNode $ir)
100.00%
1 / 1
8
100.00%
18 / 18
<?php
/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2013 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RendererGenerators;
use DOMDocument;
use DOMNode;
use DOMXPath;
use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
use s9e\TextFormatter\Configurator\RendererGenerator;
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Optimizer;
use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Serializer;
use s9e\TextFormatter\Configurator\Stylesheet;
/**
* @see docs/DifferencesInRendering.md
*/
class PHP implements RendererGenerator
{
/**
* XSL namespace
*/
const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
/**
* @var string Directory where the renderer's source is automatically saved if set, and if filepath is not set
*/
public $cacheDir;
/**
* @var string Name of the class to be created. If null, a random name will be generated
*/
public $className;
/**
* @var string Prefix used when generating a default class name
*/
public $defaultClassPrefix = 'Renderer_';
/**
* @var string If set, path to the file where the renderer will be saved
*/
public $filepath;
/**
* @var bool Whether to force non-void, empty elements to use the empty-element tag syntax in XML mode
*/
public $forceEmptyElements = true;
/**
* @var string Name of the last class generated
*/
public $lastClassName;
/**
* @var string Path to the last file saved
*/
public $lastFilepath;
/**
* @var Optimizer Optimizer
*/
public $optimizer;
/**
* @var string Output method
*/
protected $outputMethod;
/**
* @var string PHP source of generated renderer
*/
protected $php;
/**
* @var Serializer Serializer
*/
public $serializer;
/**
* @var bool Whether to use the empty-element tag syntax with non-void elements in XML mode
*/
public $useEmptyElements = true;
/**
* @var bool Whether to use the mbstring functions as a replacement for XPath expressions
*/
public $useMultibyteStringFunctions;
/**
* Constructor
*
* @param string $className Name of the class to be created
* @param string $filepath If set, path to the file where the renderer will be saved
* @return void
*/
public function __construct($cacheDir = null)
{
if (isset($cacheDir))
{
$this->cacheDir = $cacheDir;
}
if (extension_loaded('tokenizer'))
{
$this->optimizer = new Optimizer;
}
$this->useMultibyteStringFunctions = extension_loaded('mbstring');
$this->serializer = new Serializer;
}
/**
* {@inheritdoc}
*/
public function getRenderer(Stylesheet $stylesheet)
{
// Generate the source file
$php = $this->generate($stylesheet->get());
// Save the file if applicable
if (isset($this->filepath))
{
$filepath = $this->filepath;
}
elseif (isset($this->cacheDir))
{
$filepath = $this->cacheDir . '/' . str_replace('\\', '_', $this->lastClassName) . '.php';
}
if (isset($filepath))
{
file_put_contents($filepath, "<?php\n" . $php);
$this->lastFilepath = realpath($filepath);
}
// Execute the source to create the class if it doesn't exist
if (!class_exists($this->lastClassName, false))
{
eval($php);
}
// Create an instance and copy the source into the instance
$renderer = new $this->lastClassName;
$renderer->source = $php;
return $renderer;
}
/**
* Generate the source for a PHP class that renders an intermediate representation according to
* given stylesheet
*
* @param string $xsl XSL stylesheet
* @return string
*/
public function generate($xsl)
{
$header = "/**\n"
. "* @package s9e\TextFormatter\n"
. "* @copyright Copyright (c) 2010-2013 The s9e Authors\n"
. "* @license http://www.opensource.org/licenses/mit-license.php The MIT License\n"
. "*/\n\n";
// Parse the stylesheet
$ir = TemplateParser::parse($xsl);
$xpath = new DOMXPath($ir);
// Set the output method
$this->outputMethod = $ir->documentElement->getAttribute('outputMethod');
// Apply the empty-element options
$this->fixEmptyElements($ir);
// Copy some options to the serializer
$this->serializer->forceEmptyElements = $this->forceEmptyElements;
$this->serializer->outputMethod = $this->outputMethod;
$this->serializer->useEmptyElements = $this->useEmptyElements;
$this->serializer->useMultibyteStringFunctions = $this->useMultibyteStringFunctions;
// Generate the arrays of parameters, sorted by whether they are static or dynamic
$dynamicParams = [];
$staticParams = [];
foreach ($ir->getElementsByTagName('param') as $param)
{
$paramName = $param->getAttribute('name');
$paramValue = ($param->hasAttribute('select')) ? $param->getAttribute('select') : "''";
// Test whether the param value is a literal
if (preg_match('#^(?:\'[^\']*\'|"[^"]*"|[0-9]+)$#', $paramValue))
{
$staticParams[] = var_export($paramName, true) . '=>' . $paramValue;
}
else
{
$dynamicParams[] = var_export($paramName, true) . '=>' . var_export($paramValue, true);
}
}
// Start the code right after the class name, we'll prepend the header when we're done
$this->php = ' extends \\s9e\\TextFormatter\\Renderer
{
protected $htmlOutput=' . var_export($this->outputMethod === 'html', true) . ';
protected $dynamicParams=[' . implode(',', $dynamicParams) . '];
protected $params=[' . implode(',', $staticParams) . '];
protected $xpath;
public function __sleep()
{
$props = get_object_vars($this);
unset($props["out"], $props["proc"], $props["source"], $props["xpath"]);
return array_keys($props);
}
public function setParameter($paramName, $paramValue)
{
$this->params[$paramName] = (string) $paramValue;
unset($this->dynamicParams[$paramName]);
}
public function renderRichText($xml)
{
$dom = $this->loadXML($xml);
$this->xpath = new \\DOMXPath($dom);
$this->out = "";';
if ($dynamicParams)
{
$this->php .= '
foreach ($this->dynamicParams as $k => $v)
{
$this->params[$k] = $this->xpath->evaluate("string($v)", $dom);
}';
}
if ($xpath->evaluate('count(//applyTemplates[@select])'))
{
$nodesPHP = '(isset($xpath)) ? $this->xpath->query($xpath, $root) : $root->childNodes';
}
else
{
$nodesPHP = '$root->childNodes';
}
$this->php .= '
$this->at($dom->documentElement);
unset($this->xpath);
return $this->out;
}
protected function at($root, $xpath = null)
{
if ($root->nodeType === 3)
{
$this->out .= htmlspecialchars($root->textContent,' . ENT_NOQUOTES . ');
}
else
{
foreach (' . $nodesPHP . ' as $node)
{
$nodeName = $node->nodeName;';
// Remove the excess indentation
$this->php = str_replace("\n\t\t\t", "\n", $this->php);
// Collect and sort templates
$templates = [];
foreach ($ir->getElementsByTagName('template') as $template)
{
// Parse this template and save its internal representation
$irXML = $template->ownerDocument->saveXML($template);
// Get the template's match values
foreach ($template->getElementsByTagName('match') as $match)
{
$expr = $match->textContent;
$priority = $match->getAttribute('priority');
// Separate the tagName from the predicate, if any
if (preg_match('#^(\\w+)\\[(.*)\\]$#s', $expr, $m))
{
$tagName = $m[1];
$predicate = $m[2];
}
else
{
$tagName = $expr;
$predicate = '';
}
// Test whether this is a wildcard template
if (preg_match('#^(\\w+):\\*#', $tagName, $m))
{
$condition = '$node->prefix===' . var_export($m[1], true);
}
else
{
$condition = '$nodeName===' . var_export($tagName, true);
}
// Add the predicate to the condition
if ($predicate !== '')
{
$condition = '(' . $condition . '&&' . $this->serializer->convertCondition($predicate) . ')';
}
// Record this template
$templates[$priority][$irXML][] = $condition;
}
}
// Sort templates by priority descending
krsort($templates);
// Build the big if/else structure
$else = '';
foreach ($templates as $groupedTemplates)
{
// Process the grouped templates in reverse order so that the last templates apply first
// to match XSLT's default behaviour
foreach (array_reverse($groupedTemplates) as $irXML => $conditions)
{
$ir = new DOMDocument;
$ir->loadXML($irXML);
$this->php .= $else;
$else = 'else';
// If there's only one condition, remove its parentheses if applicable
if (count($conditions) === 1
&& $conditions[0][0] === '('
&& substr($conditions[0], -1) === ')')
{
$conditions[0] = substr($conditions[0], 1, -1);
}
$php = $this->serializer->serializeChildren($ir->documentElement);
if (isset($this->optimizer))
{
$php = $this->optimizer->optimize($php);
}
$this->php .= 'if(' . implode('||', $conditions) . ')';
$this->php .= '{';
$this->php .= $php;
$this->php .= '}';
}
}
// Add the default handling and close the method
$this->php .= "else \$this->at(\$node);\n\t\t\t}\n\t\t}\n\t}";
// Add the getParamAsXPath() method if necessary
if (strpos($this->php, '$this->getParamAsXPath(') !== false)
{
$this->php .= str_replace(
"\n\t\t\t\t",
"\n",
<<<'EOT'
protected function getParamAsXPath($k)
{
if (isset($this->dynamicParams[$k]))
{
return $this->dynamicParams[$k];
}
if (!isset($this->params[$k]))
{
return "''";
}
$str = $this->params[$k];
if (strpos($str, "'") === false)
{
return "'" . $str . "'";
}
if (strpos($str, '"') === false)
{
return '"' . $str . '"';
}
$toks = [];
$c = '"';
$pos = 0;
while ($pos < strlen($str))
{
$spn = strcspn($str, $c, $pos);
if ($spn)
{
$toks[] = $c . substr($str, $pos, $spn) . $c;
$pos += $spn;
}
$c = ($c === '"') ? "'" : '"';
}
return 'concat(' . implode(',', $toks) . ')';
}
EOT
);
}
// Remove the references to $this->xpath if it's never used
if (strpos($this->php, '$this->xpath->') === false)
{
$this->php = preg_replace(
[
'#\\s*\\$this->xpath\\s*=.*#',
'#\\s*unset\\(\\$this->xpath\\);#'
],
'',
$this->php
);
}
// Close the class definition
$this->php .= "\n}";
// Generate a name for that class if necessary, and save it
$className = (isset($this->className))
? $this->className
: $this->defaultClassPrefix . sha1($this->php);
$this->lastClassName = $className;
// Declare the namespace and class name
$pos = strrpos($className, '\\');
if ($pos !== false)
{
$header .= 'namespace ' . substr($className, 0, $pos) . ";\n\n";
$className = substr($className, 1 + $pos);
}
// Prepend the header and the class name
$this->php = $header . 'class ' . $className . $this->php;
return $this->php;
}
/**
* Change the IR to respect the empty-element options
*
* @param DOMNode $ir
* @return void
*/
protected function fixEmptyElements(DOMNode $ir)
{
if ($this->outputMethod !== 'xml')
{
return;
}
foreach ($ir->getElementsByTagName('element') as $element)
{
$isEmpty = $element->getAttribute('empty');
$isVoid = $element->getAttribute('void');
if ($isVoid || $isEmpty === 'no')
{
continue;
}
if (!$this->useEmptyElements)
{
$element->setAttribute('empty', 'no');
}
elseif ($isEmpty === 'maybe' && !$this->forceEmptyElements)
{
$element->setAttribute('empty', 'no');
}
}
}